<?php
/**
 * Programmer: Owen Zhang
 * Email: owen@owenzhang.com
 * Start Date: 05/15/22
 * Last Update: 05 15, 2022 [OZ]
 * Functions:
 * 时间戳+随机数+签名参数接口验证
 */

namespace app\middleware;

use app\common\ApiStatus;
use support\Redis;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class VerifySign implements MiddlewareInterface
{
    public function process(Request $request, callable $next): Response
    {
        $timestamp = $request->post('timestamp');
        $nonce     = $request->post('nonce');
        $sign      = $request->post('sign');
        $param     = $request->post();

        //时间戳校验
        $res = $this->checkTime($timestamp);
        if ($res['code'] != ApiStatus::API_SUCCESS->value) return json($res);
        //随机数校验
        $res = $this->checkNonce($nonce);
        if ($res['code'] != ApiStatus::API_SUCCESS->value) return json($res);

        //无需签名验证
        if ($request->controller === 'app\api\v1\controller\White') return $next($request);

        //签名校验
        $res = $this->checkSign($sign, $param);
        if ($res['code'] != ApiStatus::API_SUCCESS->value) return json($res);

        return $next($request);
    }

    //时间戳校验
    protected function checkTime($timestamp)
    {
        if (empty($timestamp)) {
            return ['code' => ApiStatus::API_ERROR->value, 'msg' => 'time error'];
        }
        //请求时间差是否合理值
        if (abs(time() - $timestamp) > config('app.api_timeout')) {
            return ['code' => ApiStatus::API_ERROR->value, 'msg' => 'request overtime'];
        }
        return ['code' => ApiStatus::API_SUCCESS->value, 'msg' => 'success'];
    }

    //随机数校验 16位随机数
    protected function checkNonce($nonce)
    {
        if (empty($nonce)) {
            return ['code' => ApiStatus::API_ERROR->value, 'msg' => 'random number error'];
        }
        if (strlen($nonce) > 16) {
            return ['code' => ApiStatus::API_ERROR->value, 'msg' => 'random number digit error'];
        }

        //删除过期的值
        $end = time() - config('app.api_timeout');
        Redis::zRemRangeByScore('app_nonce_list', '-inf', $end);
        //查看nonce列表
        $noncearr = Redis::zRangeByScore('app_nonce_list', '-inf', '+inf');
        if (in_array($nonce, $noncearr)) {
            return ['code' => ApiStatus::API_ERROR->value, 'msg' => 'random number repeat'];
        }

        //新标识随机数插入
        Redis::zAdd('app_nonce_list', time(), $nonce);
        return ['code' => ApiStatus::API_SUCCESS->value, 'msg' => 'success'];
    }

    //签名校验
    protected function checkSign($sign, $param)
    {
//        $sign = 'C66355D3286C6EDD637EF4FB5A5C947B';
//        $param = [
//            'mobile'=>'15221545361',
//            'type'=>'reg',
//            'uuid'=>'ffffffff-9252-a533-ffff-ffff81eff5b0',
//            'timestamp'=>'1661501888',
//            'os_type'=>'5',
//            'nonce'=>'3bKf3yGaDtm0cttY',
//            'sign'=>'C66355D3286C6EDD637EF4FB5A5C947B',
//        ];
        if (empty($sign)) {
            return ['code' => ApiStatus::API_ERROR->value, 'msg' => 'signature error'];
        }
        unset($param['sign']);
        $signLocal = $this->createSign($param);

        if ($signLocal != $sign) {
            return ['code' => ApiStatus::API_ERROR->value, 'msg' => 'illegal signature'];
        }
        return ['code' => ApiStatus::API_SUCCESS->value, 'msg' => 'success'];
    }

    /**
     * 生成接口sign
     * 将获得的signStr进行md5加密候转化成大写,再拼接密钥，再md5加密，并且转化为大写
     * 签名生成的通用步骤如下：
     *
     * 第一步，设所有发送的数据非空参数值的参数按照参数名ASCII码从小到大排序（字典序），
     * 使用URL键值对的格式（即key1:value1key2:value2…）拼接成字符串signStr。
     *
     * 特别注意以下重要规则：
     *
     * ◆ 参数名ASCII码从小到大排序（字典序）；
     * ◆ 如果参数的值为空不参与签名；
     * ◆ 参数名区分大小写;
     * (((注意：不同环境后端框架md5结果不一样为，32/16位大小写，此处得前后端对应
     * https://www.sojson.com/md5/ 来猜是具体32/16位大小写 哪一种加密
     * 以下结果是 32位小写 md5加密的结果)))
     * 将获得的signStr进行md5加密候转化成大写,再拼接密钥，再md5加密，并且转化为大写,
     * 密钥与后端商定
     * 假设传送的参数如下：
     *
     * timestamp： 1651226218
     * nonce： cpNrX8wVBOhnIPTs
     * id： 1
     * name： zhang欧文
     * uuid： ffffffff-9252-a533-ffff-ffff81eff5b0
     * os_type： 3
     * 第一步：
     * signStr="id1namezhang欧文noncecpNrX8wVBOhnIPTsos_type3timestamp1651226218uuidffffffff-9252-a533-ffff-ffff81eff5b0"
     * 第二步：
     * strtoupper(md5(strtoupper(md5(signStr)).密钥)); （密钥=0e15a974-c9f4-46f3-a3e8-290ec986701c）
     * 第三步：
     * signStr=69AE2F1DDBE6D78699A087EE62F8FE63
     *
     * @return string signStr
     */
    public function createSign($params)
    {
        $key = config('app.sign_salt');
        $str = $this->createSignStr($params);
        return strtoupper(md5(strtoupper(md5($str)) . $key));
    }

    /**
     * 生成接口signStr
     * 获取到的请求参数并按照参数名ASCII码从小到大排序,如果值是bool需转换成0，1表示，如果值是数组，需重新调用接口获取
     *
     * @return string signStr
     */
    public function createSignStr($params)
    {
        ksort($params);
        $sign = '';
        foreach ($params as $key => $val) {
            if ($val == null) continue;
            if (is_bool($val)) $val = ($val) ? 1 : 0;
            $sign .= $key . ((is_array($val)) ? $this->createSignStr($val) : $val);
        }
        return $sign;
    }
}